package com.panopset;

import java.io.File;
import java.io.StringWriter;
import java.util.Collections;
import java.util.Set;
import java.util.TreeSet;

import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * Provides list of maven dependencies from any pom, provided that the project
 * has been built. Traverses the .m2 repository for dependency hierarchy.
 *
 * @author Karl Dinwiddie
 *
 */
public final class MavenAuditor {

    /**
     * @param pomFile
     *            Any valid pom.xml file.
     * @return Sorted set of MavenDependency objects.
     * @throws Exception
     *             Exception.
     */
    public Set<MavenDependency> loadPomDependencies(final File pomFile)
            throws Exception {
        final Set<MavenDependency> rtn = Collections
                .synchronizedSortedSet(new TreeSet<MavenDependency>());
        SAXParserFactory.newInstance().newSAXParser()
                .parse(pomFile, new DefaultHandler() {

                    /**
                     * Parent project flag.
                     */
                    private boolean parent = false;

                    private StringBuffer sb = new StringBuffer();

                    @Override
                    public void characters(final char[] buffer,
                            final int start, final int length) {
                        sb.append(buffer, start, length);
                    }

                    /**
                     * Maven dependency.
                     */
                    private MavenDependency dep;

                    /**
                     * Project to be included as well.
                     */
                    private MavenDependency project;

                    @Override
                    public void startElement(final String uri,
                            final String localName, final String qName,
                            final Attributes attributes) {
                        sb = new StringBuffer();
                        if ("dependency".equals(qName)) {
                            dep = new MavenDependency(pomFile);
                        } else if ("parent".equals(qName)) {
                            dep = null;
                            parent = true;
                        } else if ("project".equals(qName)) {
                            project = new MavenDependency(pomFile);
                        }
                    }

                    @Override
                    public void endElement(final String uri,
                            final String localName, final String qName)
                            throws SAXException {
                        if (qName == null) {
                            return;
                        }
                        if ("parent".equals(qName)) {
                            parent = false;
                            return;
                        }
                        if (parent) {
                            return;
                        }
                        if ("project".equals(qName)) {
                            for (MavenDependency md : rtn) {
                                md.setParent(project);
                            }
                            rtn.remove(project);
                            rtn.add(project);
                        } else if ("dependency".equals(qName)) {
                            File repositoryPom = new File(dep
                                    .getLocalRepositoryPom());
                            if (!rtn.contains(dep) && repositoryPom.exists()
                                    && repositoryPom.isFile()) {
                                try {
                                    for (MavenDependency md
                                        : loadPomDependencies(repositoryPom)) {
                                        rtn.remove(md);
                                        rtn.add(md);
                                    }
                                } catch (Exception e) {
                                    Util.log(e);
                                }
                            }
                            rtn.remove(dep);
                            rtn.add(dep);
                            dep = null;
                        } else if ("scope".equals(qName)) {
                            if (dep != null) {
                                dep.depScope = sb.toString();
                            }
                        } else if ("groupId".equals(qName)) {
                            if (dep == null) {
                                if (project.depGroupId == null) {
                                    project.depGroupId = sb.toString();
                                }
                            } else {
                                dep.depGroupId = sb.toString();
                            }
                        } else if ("artifactId".equals(qName)) {
                            if (dep == null) {
                                if (project.depArtifactId == null) {
                                    project.depArtifactId = sb.toString();
                                }
                            } else {
                                dep.depArtifactId = sb.toString();
                            }
                        } else if ("version".equals(qName)) {
                            if (dep == null) {
                                if (project.depVersion == null) {
                                    project.depVersion = sb.toString();
                                }
                            } else {
                                dep.depVersion = sb.toString();
                            }
                        } else if ("optional".equals(qName)) {
                            dep.optional = sb.toString();
                        }
                    }
                });
        return rtn;
    }

    /**
     * Maven home.
     */
    private static String mavenHome;

    /**
     * @return System.getProperty("user.home")/.m2/repository.
     */
    public static String getMavenHome() {
        if (mavenHome == null) {
            StringWriter sw = new StringWriter();
            sw.append(System.getProperty("user.home"));
            sw.append(Strings.FILE_SEP);
            sw.append(".m2");
            sw.append(Strings.FILE_SEP);
            sw.append("repository");
            mavenHome = sw.toString();
        }
        return mavenHome;
    }

    /**
     * Maven dependency.
     */
    public final class MavenDependency implements Comparable<MavenDependency> {

        /**
         * Is parent set flag.
         */
        private boolean isParentSet = false;

        /**
         * Parent.
         */
        private MavenDependency parent;

        /**
         * @param parentProject
         *            Parent project.
         */
        public void setParent(final MavenDependency parentProject) {
            if (!isParentSet) {
                parent = parentProject;
                isParentSet = true;
            }
        }

        /**
         * @return Parent dependency or empty String if this is top level.
         */
        public MavenDependency getParent() {
            if (parent == null) {
                parent = this;
            }
            return parent;
        }

        /**
         * @param pomFile
         *            pom file.
         */
        public MavenDependency(final File pomFile) {
            pom = pomFile;
        }

        /**
         * @return True only if dependency is required, and not test scope.
         */
        public boolean isRequired() {
            if (isOptional()) {
                return false;
            }
            if ("test".equalsIgnoreCase(getScope())) {
                return false;
            }
            return true;
        }

        /**
         * Pom file this dependency was found in.
         */
        private final File pom;

        /**
         * @return the groupId
         */
        public String getGroupId() {
            if (depGroupId == null) {
                depGroupId = "";
            }
            return depGroupId;
        }

        /**
         * @param groupId
         *            the groupId to set
         */
        public void setGroupId(final String groupId) {
            this.depGroupId = groupId;
        }

        /**
         * @return the artifactId
         */
        public String getArtifactId() {
            if (depArtifactId == null) {
                depArtifactId = "";
            }
            return depArtifactId;
        }

        /**
         * @param artifactId
         *            the artifactId to set
         */
        public void setArtifactId(final String artifactId) {
            this.depArtifactId = artifactId;
        }

        /**
         * @return the version
         */
        public String getVersion() {
            if (depVersion == null) {
                depVersion = "";
            }
            return depVersion;
        }

        /**
         * @param version
         *            the version to set
         */
        public void setVersion(final String version) {
            this.depVersion = version;
        }

        /**
         * @return the scope
         */
        public String getScope() {
            if (depScope == null) {
                depScope = "";
            }
            return depScope;
        }

        /**
         * @param scope
         *            the scope to set
         */
        public void setScope(final String scope) {
            this.depScope = scope;
        }

        /**
         * @return pom file.
         */
        public File getPom() {
            return pom;
        }

        /**
         * Group id.
         */
        private String depGroupId;

        /**
         * Artifact id.
         */
        private String depArtifactId;

        /**
         * Version.
         */
        private String depVersion;

        /**
         * Optional.
         */
        private String optional;

        /**
         * Scope.
         */
        private String depScope;

        @Override
        public int hashCode() {
            return getGroupId().hashCode() + getArtifactId().hashCode()
                    + getVersion().hashCode();
        }

        @Override
        public boolean equals(final Object obj) {
            if (obj == null) {
                return false;
            }
            if (!(obj instanceof MavenAuditor.MavenDependency)) {
                return false;
            }
            MavenAuditor.MavenDependency o = (MavenAuditor.MavenDependency) obj;
            return this.getId().equals(o.getId());
        }

        /**
         * @return id for comparisons.
         */
        private String getId() {
            return getArtifactId() + ";" + getGroupId() + ";" + getVersion();
        }

        /**
         * @return Path to pom in local repository.
         */
        public String getRepository() {
            StringWriter sw = new StringWriter();
            sw.append(getGroupId().replace(".", Strings.FILE_SEP));
            sw.append(Strings.FILE_SEP);
            sw.append(getArtifactId());
            sw.append(Strings.FILE_SEP);
            sw.append(getVersion());
            return sw.toString();
        }

        /**
         * @return True if dependency is marked optional in the pom.
         */
        public boolean isOptional() {
            if (Strings.isPopulated(optional)
                    && "true".equalsIgnoreCase(optional)) {
                return true;
            }
            return false;
        }

        /**
         * @return name.
         */
        public String getName() {
            StringWriter sw = new StringWriter();
            sw.append(getArtifactId());
            sw.append("-");
            sw.append(getVersion());
            return sw.toString();
        }

        /**
         * @return Relative path to pom.
         */
        public String getRelativeRepositoryPom() {
            StringWriter sw = new StringWriter();
            sw.append(getRepository());
            sw.append(Strings.FILE_SEP);
            sw.append(getPomName());
            return sw.toString();
        }

        /**
         * @return Relative path to jar.
         */
        public String getRelativeRepositoryJar() {
            StringWriter sw = new StringWriter();
            sw.append(getRepository());
            sw.append(Strings.FILE_SEP);
            sw.append(getJarName());
            return sw.toString();
        }

        /**
         * @return Path to pom in local repository.
         */
        public String getLocalRepositoryPom() {
            StringWriter sw = new StringWriter();
            sw.append(getMavenHome());
            sw.append(Strings.FILE_SEP);
            sw.append(getRelativeRepositoryPom());
            return sw.toString();
        }

        /**
         * @return Path to jar in local repository.
         */
        public String getLocalRepositoryJar() {
            StringWriter sw = new StringWriter();
            sw.append(getMavenHome());
            sw.append(Strings.FILE_SEP);
            sw.append(getRelativeRepositoryJar());
            return sw.toString();
        }

        /**
         * @return Simple jar name.
         */
        public String getJarName() {
            StringWriter sw = new StringWriter();
            sw.append(getName());
            sw.append(".jar");
            return sw.toString();
        }

        /**
         * @return Simple pom name.
         */
        public String getPomName() {
            StringWriter sw = new StringWriter();
            sw.append(getName());
            sw.append(".pom");
            return sw.toString();
        }

        @Override
        public String toString() {
            StringWriter sw = new StringWriter();
            sw.append("Group:");
            sw.append(getGroupId());
            sw.append(" Artifact:");
            sw.append(getArtifactId());
            sw.append(" Version:");
            sw.append(getVersion());
            sw.append(" Scope:");
            sw.append(getScope());
            return sw.toString();
        }

        @Override
        public int compareTo(final MavenDependency o) {
            return getId().compareTo(o.getId());
        }
    }
}
